//	Ordinaire.Database.ORMapping
//	Copyright (c) 2008 Jonathan Loe
//
//	MIT license (http://en.wikipedia.org/wiki/MIT_License)
//
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights 
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
//	of the Software, and to permit persons to whom the Software is furnished to do so, 
//	subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in all 
//	copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
//	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//	PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
//	FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
//	ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Data;
using System.Xml;
using System.Text;

namespace Ordinaire.Db
{
	public enum MembersMode { Auto, Exact };

	/// <summary>
	/// ORMapping is a helper class of Ordinaire.Database.ORMapper class for a single mapping.
	/// </summary>
	/// <seealso cref="Ordinaire.Database.ORMapper"/>
	public class ORMapping
	{
		#region Variables
		/// <summary>
		/// List of element names used in the mapping file.
		/// </summary>
		public struct ElementNames
		{
			public const string MAPPING = "mapping";
			public const string COMMAND_TEXT = "commandText";
			public const string OBJECT = "object";
			public const string PARAMS = "params";
			public const string PARAM = "param";
			public const string MEMBERS = "members";
			public const string MEMBER = "member";
		}

		/// <summary>
		/// List of attribute names used in the mapping file.
		/// </summary>
		public struct AttributeNames
		{
			public const string NAME = "name";
			public const string MODE = "mode";
			public const string TYPE = "type";
			public const string ASSEMBLY_NAME = "assemblyName";
			public const string TYPE_NAME = "typeName";
			public const string COLUMN_NAME = "columnName";
			public const string PROPERTY_NAME = "propertyName";
		}

		/// <summary>
		/// List of available values of <commandText type="[text|sp]"></commandText>
		/// </summary>
		private struct CommandTypes
		{
			public const string TEXT = "text";
			public const string STORED_PROCEDURE = "sp";
		}

		/// <summary>
		/// List of available values of <members type="[auto|exact]"></members>
		/// </summary>
		private struct MembersModes
		{
			public const string AUTO = "auto";
			public const string EXACT = "exact";
		}

		/// <summary>
		/// Root element name
		/// </summary>
		private const string ROOT_NAME = "mapping";

		private Dictionary<string, string> _members;

		private XmlNode _node;
		private XmlNode _nodeCommandText;
		private XmlNode _nodeObject;
		private XmlNode _nodeMembers;
		#endregion

		#region Constructor
		/// <summary>
		/// Create new instance of Ordinaire.Database.ORMapping class. No validation is performed.
		/// </summary>
		/// <param name="mappingNode">element node</param>
		public ORMapping(XmlNode mappingNode) : this (mappingNode, false) { }

		/// <summary>
		/// Create new instance of Ordinaire.Database.ORMapping class.
		/// </summary>
		/// <param name="mappingNode">element node</param>
		/// <param name="validate">whether to validate the node</param>
		public ORMapping(XmlNode mappingNode, bool validate)
		{
			if (mappingNode == null) { throw new ArgumentNullException("mappingNode"); }

			if (validate) { Validate(mappingNode); }

			_node = mappingNode;

			_nodeCommandText = _node[ElementNames.COMMAND_TEXT];
			_nodeObject = _node[ElementNames.OBJECT];
			_nodeMembers = _node[ElementNames.MEMBERS];
		}
		#endregion

		#region Properties
		/// <summary>
		/// Returns command text.
		/// </summary>
		public string CommandText
		{
			get
			{
				if (_nodeCommandText == null) { return String.Empty; }

				string commandText = _nodeCommandText.InnerText;

				if (String.IsNullOrEmpty(commandText)) { return String.Empty; }
				commandText = commandText.Trim(); // clean white spaces

				// removes white spaces within
				string[] commandPerLine = commandText.Split(new string[] { "\r\n", " ", "\t" }, StringSplitOptions.RemoveEmptyEntries);
				commandText = String.Join(" ", commandPerLine);

				return commandText;
			}
		}

		/// <summary>
		/// Returns command type, either text or stored procedure
		/// </summary>
		public CommandType CommandType
		{
			get
			{
				if (_nodeCommandText == null) { return CommandType.Text; }
				XmlAttribute xa = _nodeCommandText.Attributes[AttributeNames.TYPE];
				if (xa == null) { return CommandType.Text; } // default is text

				switch (xa.Value)
				{
					case CommandTypes.TEXT: return CommandType.Text;
					case CommandTypes.STORED_PROCEDURE: return CommandType.StoredProcedure;
					default: return CommandType.Text; // set as text for unrecognised value
				}
			}
		}

		/// <summary>
		/// Returns the 'object' node.
		/// </summary>
		public XmlNode Object
		{
			get { return _nodeObject; }
		}

		/// <summary>
		/// Returns the assembly file name.
		/// </summary>
		public string ObjectAssemblyName
		{
			get
			{
				if (_nodeObject == null) { return String.Empty; }
				XmlAttribute xa = _nodeObject.Attributes[AttributeNames.ASSEMBLY_NAME];
				if (xa == null) { return String.Empty; }
				return xa.Value;
			}
		}

		/// <summary>
		/// Returns the object type name.
		/// </summary>
		public string ObjectTypeName
		{
			get
			{
				if (_nodeObject == null) { return String.Empty; }
				XmlAttribute xa = _nodeObject.Attributes[AttributeNames.TYPE_NAME];
				if (xa == null) { return String.Empty; }
				return xa.Value;
			}
		}

		/// <summary>
		/// Returns mode of the members.
		/// </summary>
		public MembersMode MembersMode
		{
			get
			{
				if (_nodeMembers == null) { return MembersMode.Auto; }
				XmlAttribute xa = _nodeMembers.Attributes[AttributeNames.MODE];
				if (xa == null) { return MembersMode.Auto; } // default is auto

				switch (xa.Value)
				{
					case MembersModes.AUTO: return MembersMode.Auto;
					case MembersModes.EXACT: return MembersMode.Exact;
					default: return MembersMode.Auto; // set as auto for unrecognised value
				}
			}
		}
		#endregion

		#region Public methods
		/// <summary>
		/// Returns listed members. Member is a pair of property name and column name.
		/// </summary>
		public Dictionary<string, string> Members
		{
			get
			{
				if (_members != null) { return _members; }

				_members = new Dictionary<string, string>();
				if (_nodeMembers == null || _nodeMembers.ChildNodes.Count == 0) { return _members; }

				foreach (XmlNode item in _nodeMembers.ChildNodes)
				{
					if (!item.Name.Equals(ElementNames.MEMBER)) { continue; }

					XmlAttribute xaPropertyName = item.Attributes[AttributeNames.PROPERTY_NAME];
					string propertyName = (xaPropertyName == null) ? String.Empty : xaPropertyName.Value;
					XmlAttribute xaColumnName = item.Attributes[AttributeNames.COLUMN_NAME];
					string columnName = (xaColumnName == null) ? String.Empty : xaColumnName.Value;
					_members.Add(propertyName, columnName);
					_members.Add(columnName, propertyName);
				}

				return _members;
			}
		}

		/// <summary>
		/// Validates mapping node.
		/// </summary>
		/// <param name="mapping">mapping node</param>
		public static void Validate(XmlNode mapping)
		{
			if (mapping == null) { throw new ArgumentNullException("mapping"); }

			// Validates mapping name
			if (!mapping.Name.Equals(ElementNames.MAPPING))
			{
				throw new FormatException(
					String.Format(ExceptionMessage.InvalidElementFormat, mapping.Name)
				);
			}

			// Validates <command>
			XmlElement eCommand = mapping[ElementNames.COMMAND_TEXT];
			if (eCommand == null)
			{
				throw new FormatException(
					String.Format(ExceptionMessage.MissingElementFormat, ElementNames.COMMAND_TEXT)
				);
			}

			// Validates <object>
			XmlElement eObject = mapping[ElementNames.OBJECT];
			if (eObject != null)
			{
				if (eObject.Attributes[AttributeNames.ASSEMBLY_NAME] == null)
				{
					throw new FormatException(
						String.Format(
							ExceptionMessage.MissingAttributeInElementFormat,
							AttributeNames.ASSEMBLY_NAME,
							ElementNames.OBJECT
						)
					);
				}
				else if (String.IsNullOrEmpty(eObject.Attributes[AttributeNames.ASSEMBLY_NAME].Value.Trim()))
				{
					throw new FormatException(
						String.Format(
							ExceptionMessage.EmptyAttributeInElementFormat,
							AttributeNames.ASSEMBLY_NAME,
							ElementNames.OBJECT
						)
					);
				}
				else if (eObject.Attributes[AttributeNames.TYPE_NAME] == null)
				{
					throw new FormatException(
						String.Format(
							ExceptionMessage.MissingAttributeInElementFormat,
							AttributeNames.TYPE_NAME,
							ElementNames.OBJECT
						)
					);
				}
				else if (String.IsNullOrEmpty(eObject.Attributes[AttributeNames.TYPE_NAME].Value.Trim()))
				{
					throw new FormatException(
						String.Format(
							ExceptionMessage.EmptyAttributeInElementFormat,
							AttributeNames.TYPE_NAME,
							ElementNames.OBJECT
						)
					);
				}
			}

			// Validates <members>
			XmlElement eMembers = mapping[ElementNames.MEMBERS];
			if (eMembers != null)
			{
				if (eMembers.ChildNodes == null || eMembers.ChildNodes.Count == 0)
				{
					throw new FormatException(
						String.Format(ExceptionMessage.MissingElementFormat, ElementNames.MEMBER)
					);
				}

				for (int i = 0; i < eMembers.ChildNodes.Count; i++)
				{
					XmlNode eMember = eMembers.ChildNodes[i];

					// ignores comment line
					if (eMember.Name.Equals("#comment")) { continue; }

					if (!eMember.Name.Equals(ElementNames.MEMBER))
					{
						throw new FormatException(
							String.Format(ExceptionMessage.InvalidElementFormat, eMember.Name)
						);
					}

					if (eMember.Attributes[AttributeNames.COLUMN_NAME] == null)
					{
						throw new FormatException(
							String.Format(
								ExceptionMessage.MissingAttributeInElementFormat,
								AttributeNames.COLUMN_NAME,
								ElementNames.MEMBER
							)
						);
					}
					if (eMember.Attributes[AttributeNames.PROPERTY_NAME] == null)
					{
						throw new FormatException(
							String.Format(
								ExceptionMessage.MissingAttributeInElementFormat,
								AttributeNames.PROPERTY_NAME,
								ElementNames.MEMBER
							)
						);
					}
				}
			}
		}
		#endregion
	}
}
